﻿@page "/"
@using System.Buffers
@using System.Diagnostics
@using System.Security.Cryptography
@using System.Net.Http.Headers
@using System.Runtime.CompilerServices
@using global::Shared

<p>支持随时取消的多文件并行分片上传，示例同时上传2个文件，每个文件同时上传2个分片，合计同时上传4个分片</p>
<InputFile OnChange="UploadFile" multiple></InputFile>
<button @onclick="async (MouseEventArgs e) => uploadCancelSource?.Cancel()">取消上传</button>

@code {
    [Inject] private HttpClient _http { get; init; } = null!;
    [Inject] private ILogger<Index> _logger { get; init; } = null!;

    private CancellationTokenSource? uploadCancelSource;

    /// <summary>
    /// 上传文件
    /// </summary>
    /// <param name="args">上传文件的事件参数</param>
    /// <returns></returns>
    private async Task UploadFile(InputFileChangeEventArgs args)
    {
        // 设置文件并发选项
        var parallelCts = new CancellationTokenSource();
        uploadCancelSource = parallelCts;
        var parallelOption = new ParallelOptions
        {
            MaxDegreeOfParallelism = 2,
            CancellationToken = parallelCts.Token
        };

        // 并发上传所有文件
        await Parallel.ForEachAsync(
            args.GetMultipleFiles(int.MaxValue),
            parallelOption,
            async (file, cancellation) =>
            {
                // 这里的取消令牌是并发方法创建的，和并发选项里的令牌不是一个
                if (cancellation.IsCancellationRequested)
                {
                    parallelCts.Cancel();
                    return;
                }

                // 使用链接令牌确保外部取消能传递到内部
                var chunkUploadResult = await UploadChunkedFile(
                    file,
                    CancellationTokenSource.CreateLinkedTokenSource(
                        parallelCts.Token,
                        cancellation
                    ).Token
                );

                // 如果上传不成功则取消后续上传
                if (chunkUploadResult != FileUploadResult.Success)
                {
                    parallelCts.Cancel();
                    return;
                }
            }
        );
    }

    /// <summary>
    /// 分片上传文件
    /// </summary>
    /// <param name="file">要上传的文件</param>
    /// <param name="cancellation">取消令牌</param>
    /// <returns>上传结果</returns>
    private async Task<FileUploadResult> UploadChunkedFile(IBrowserFile file, CancellationToken cancellation = default)
    {
        if (cancellation.IsCancellationRequested) return FileUploadResult.Canceled;

        _logger.LogInformation("开始上传文件：{0}", file.Name);

        // 计算分片大小，文件小于10MB分片1MB，大于100MB分片10MB，在其间则使用不超过10片时的所需大小
        var coefficient = file.Size switch
        {
            <= 1024 * 1024 * 10 => 1,
            > 1024 * 1024 * 10 and <= 1024 * 1024 * 100 => (int)Math.Ceiling(file.Size / (1024.0 * 1024) / 10),
            _ => 10
        };

        // 初始化分片参数，准备字符串格式的数据供表单使用
        var bufferSize = 1024 * 1024 * coefficient; // MB
        var stringBufferSize = bufferSize.ToString();
        var chunkCount = (int)Math.Ceiling(file.Size / (double)bufferSize);
        var stringChunkCount = chunkCount.ToString();
        var stringFileSize = file.Size.ToString();

        // 发起分片上传，协商Hash算法，获取任务代码
        var uploadStartContent = new List<KeyValuePair<string, string>>
        {
            new("uploadType", "startUpload"),
            new("fileName", file.Name),
            new("fileSize", stringFileSize),
            new("allowedHashAlgorithm", "SHA1"),
            new("allowedHashAlgorithm", "SHA256"),
            new("fileChunkCount", stringChunkCount),
            new("fileChunkSize", stringBufferSize),
        };

        var uploadStartForm = new FormUrlEncodedContent(uploadStartContent);

        HttpResponseMessage? uploadStartResponse = null;
        try
        {
            uploadStartResponse = await _http.PostAsync("/upload", uploadStartForm, cancellation);
        }
        catch (TaskCanceledException e)
        {
            _logger.LogWarning(e, "外部取消上传，已停止文件：{0} 的上传", file.Name);
            return FileUploadResult.Canceled;
        }
        catch (Exception e)
        {
            _logger.LogError(e, "文件：{0} 的上传参数协商失败", file.Name);
            return FileUploadResult.Fail;
        }

        // 如果服务器响应失败，结束上传
        if (uploadStartResponse?.IsSuccessStatusCode is null or false)
        {
            _logger.LogError("文件：{0} 的上传参数协商失败", file.Name);
            return FileUploadResult.Fail;
        }

        // 解析协商的参数
        var uploadStartReault = await uploadStartResponse.Content.ReadFromJsonAsync<FileChunkUploadStartReault>();
        var uploadTaskCode = uploadStartReault!.UploadTaskCode;
        var selectedHashAlgorithm = uploadStartReault!.SelectedHashAlgorithm;

        _logger.LogInformation("文件：{0} 的上传参数协商成功", file.Name);

        // 设置分片并发选项
        var parallelOption = new ParallelOptions
        {
            MaxDegreeOfParallelism = 2,
        };

        var fileUploadCancelSource = new CancellationTokenSource();
        var sliceEnumeratorCancelSource = CancellationTokenSource
            .CreateLinkedTokenSource(
                cancellation,
                fileUploadCancelSource.Token
            );
        // 各个分片的上传结果
        var sliceUploadResults = new FileUploadResult?[chunkCount];
        // 并发上传各个分片，并发循环本身不能用并发选项的取消令牌取消，可能会导致内存泄漏，应该通过切片循环的取消使并发循环因没有可用元素自然结束
        await Parallel.ForEachAsync(
            SliceFileAsync(
                file,
                bufferSize,
                sliceEnumeratorCancelSource.Token
            ),
            parallelOption,
            async (fileSlice, sliceUploadCancel) =>
            {
                // 解构参数
                var (memory, sliceIndex, readBytes, fileOffset) = fileSlice;

                // 使用using确保结束后把租用的内存归还给内存池
                using (memory)
                {
                    var stringSliceIndex = sliceIndex.ToString();

                    // 主动取消上传，发送取消请求，通知服务端清理资源
                    if (sliceUploadCancel.IsCancellationRequested)
                    {
                        _logger.LogWarning("外部取消上传，已停止文件：{0} 的上传", file.Name);

                        fileUploadCancelSource.Cancel();
                        sliceUploadResults[sliceIndex] = FileUploadResult.Canceled;

                        var uploadCancelContent = new Dictionary<string, string>()
                        {
                            { "uploadType", "cancelUpload" },
                            { "uploadTaskCode", uploadTaskCode! },
                            { "fileName", file.Name },
                            { "hashAlgorithm", selectedHashAlgorithm },
                            { "fileChunkCount", stringChunkCount },
                            { "fileChunkIndex", stringSliceIndex },
                            { "cancelReason", "调用方要求取消上传。" },
                        };
                        var uploadCancelForm = new FormUrlEncodedContent(uploadCancelContent);
                        var uploadCancelResponse = await _http.PostAsync("/upload", uploadCancelForm);

                        return;
                    }

                    // 当前上传分片索引应当小于预计的分片数
                    Debug.Assert(sliceIndex < chunkCount);

                    // 获取准确大小的缓冲区，从内存池租用时得到的容量可能大于申请的大小，使用C#的新集合切片语法
                    var readBuffer = memory.Memory[..readBytes];

                    var sw = Stopwatch.StartNew();
                    // 根据协商的算法计算Hash，wasm环境不支持MD5和全部非对称加密算法
                    var hash = selectedHashAlgorithm switch
                    {
                        "SHA1" => SHA1.HashData(readBuffer.Span),
                        "SHA256" => SHA256.HashData(readBuffer.Span),
                        _ => Array.Empty<byte>()
                    };
                    sw.Stop();

                    _logger.LogInformation("文件：{0} 的片段 {1}({2} Bytes) 计算Hash用时 {3}", file.Name, sliceIndex, readBytes, sw.Elapsed);

                    var stringReadBytes = readBytes.ToString();
                    var stringFileOffset = fileOffset.ToString();

                    // 上传当前分片
                    MultipartFormDataContent uploadFileForm = new();
                    uploadFileForm.Add(new StringContent(uploadTaskCode!), "uploadTaskCode");
                    uploadFileForm.Add(new StringContent("uploadChunk"), "uploadType");
                    uploadFileForm.Add(new StringContent(file.Name), "fileName");
                    uploadFileForm.Add(new StringContent(stringFileSize), "fileSize");
                    uploadFileForm.Add(new StringContent(selectedHashAlgorithm!), "hashAlgorithm");
                    uploadFileForm.Add(new StringContent(hash.ToHexString()), "hashValue");
                    uploadFileForm.Add(new StringContent(stringChunkCount), "fileChunkCount");
                    uploadFileForm.Add(new StringContent(stringReadBytes), "fileChunkSize");
                    uploadFileForm.Add(new StringContent(stringFileOffset), "fileChunkOffset");
                    uploadFileForm.Add(new StringContent(stringSliceIndex), "fileChunkIndex");

                    // 如果是未知的文件类型，设置为普通二进制流的MIME类型
                    var fileChunk = new ReadOnlyMemoryContent(readBuffer);
                    fileChunk.Headers.ContentType = new MediaTypeHeaderValue(string.IsNullOrEmpty(file.ContentType) ? "application/octet-stream" : file.ContentType);
                    uploadFileForm.Add(fileChunk, "fileChunkData", file.Name);

                    HttpResponseMessage? uploadResponse = null;
                    try
                    {
                        var uploadTaskCancel = CancellationTokenSource
                            .CreateLinkedTokenSource(
                                sliceUploadCancel,
                                sliceEnumeratorCancelSource.Token
                            );

                        _logger.LogInformation("文件：{0} 的片段 {1}({2} Bytes) 开始上传", file.Name, sliceIndex, readBytes);

                        sw.Restart();
                        uploadResponse = await _http.PostAsync("/upload", uploadFileForm, uploadTaskCancel.Token);
                    }
                    catch (TaskCanceledException e)
                    {
                        _logger.LogWarning(e, "外部取消上传，已停止文件：{0} 的上传", file.Name);

                        fileUploadCancelSource.Cancel();
                        sliceUploadResults[sliceIndex] = FileUploadResult.Canceled;

                        var uploadCancelContent = new Dictionary<string, string>()
                        {
                            { "uploadType", "cancelUpload" },
                            { "uploadTaskCode", uploadTaskCode! },
                            { "fileName", file.Name },
                            { "hashAlgorithm", selectedHashAlgorithm },
                            { "fileChunkCount", stringChunkCount },
                            { "fileChunkIndex", stringSliceIndex },
                            { "cancelReason", "调用方要求取消上传。" },
                        };
                        var uploadCancelForm = new FormUrlEncodedContent(uploadCancelContent);
                        var uploadCancelResponse = await _http.PostAsync("/upload", uploadCancelForm);

                        return;
                    }
                    catch (Exception e)
                    {
                        _logger.LogError(e, "上传发生错误，已停止文件：{0} 的上传", file.Name);

                        fileUploadCancelSource.Cancel();
                        sliceUploadResults[sliceIndex] = FileUploadResult.Fail;

                        var uploadCancelContent = new Dictionary<string, string>()
                        {
                            { "uploadType", "cancelUpload" },
                            { "uploadTaskCode", uploadTaskCode! },
                            { "fileName", file.Name },
                            { "hashAlgorithm", selectedHashAlgorithm },
                            { "fileChunkCount", stringChunkCount },
                            { "fileChunkSize", stringReadBytes },
                            { "fileChunkOffset", stringFileOffset },
                            { "fileChunkIndex", stringSliceIndex },
                            { "cancelReason", "上传过程中发生错误。" },
                        };
                        var uploadCancelForm = new FormUrlEncodedContent(uploadCancelContent);
                        var uploadCancelResponse = await _http.PostAsync("/upload", uploadCancelForm);

                        return;
                    }
                    finally
                    {
                        sw.Stop();
                    }

                    // 上传发生错误，发送取消请求，通知服务端清理资源
                    if (uploadResponse?.IsSuccessStatusCode is null or false)
                    {
                        _logger.LogError("上传发生错误，已停止文件：{0} 的上传", file.Name);

                        fileUploadCancelSource.Cancel();
                        sliceUploadResults[sliceIndex] = FileUploadResult.Fail;

                        var uploadCancelContent = new Dictionary<string, string>()
                        {
                            { "uploadType", "cancelUpload" },
                            { "uploadTaskCode", uploadTaskCode! },
                            { "fileName", file.Name },
                            { "hashAlgorithm", selectedHashAlgorithm },
                            { "fileChunkCount", stringChunkCount },
                            { "fileChunkSize", stringReadBytes },
                            { "fileChunkOffset", stringFileOffset },
                            { "fileChunkIndex", stringSliceIndex },
                            { "cancelReason", "上传过程中发生错误。" },
                        };
                        var uploadCancelForm = new FormUrlEncodedContent(uploadCancelContent);
                        var uploadCancelResponse = await _http.PostAsync("/upload", uploadCancelForm);

                        return;
                    }

                    _logger.LogInformation("文件：{0} 的片段 {1}({2} Bytes) 上传成功，用时 {3}", file.Name, sliceIndex, readBytes, sw.Elapsed);

                    sliceUploadResults[sliceIndex] = FileUploadResult.Success;
                }
            }
        );

        // 如果所有分片都上传成功，则发送完成请求完成上传
        if (sliceUploadResults.All(success => success is FileUploadResult.Success))
        {
            var uploadCompleteContent = new Dictionary<string, string>()
            {
                {"uploadType", "completeUpload"},
                {"uploadTaskCode", uploadTaskCode!},
                {"fileName", file.Name},
                {"fileSize", stringFileSize},
                {"hashAlgorithm", selectedHashAlgorithm},
                {"fileChunkCount", stringChunkCount},
                {"fileChunkSize", stringBufferSize},
            };
            var uploadCompleteForm = new FormUrlEncodedContent(uploadCompleteContent);
            var uploadCompleteResponse = await _http.PostAsync("/upload", uploadCompleteForm);

            if (uploadCompleteResponse.IsSuccessStatusCode)
            {
                _logger.LogInformation("文件：{0} 上传成功，共 {1} 个片段", file.Name, chunkCount);
                return FileUploadResult.Success;
            }
            else
            {
                _logger.LogError("上传发生错误，已停止文件：{0} 的上传", file.Name);

                var uploadCancelContent = new Dictionary<string, string>()
                {
                    {"uploadType", "cancelUpload"},
                    {"uploadTaskCode", uploadTaskCode!},
                    {"fileName", file.Name},
                    {"hashAlgorithm", selectedHashAlgorithm},
                    {"fileChunkCount", stringChunkCount},
                    {"cancelReason", "上传过程中发生错误。"},
                };
                var uploadCancelForm = new FormUrlEncodedContent(uploadCancelContent);
                var uploadCancelResponse = await _http.PostAsync("/upload", uploadCancelForm);

                return FileUploadResult.Fail;
            }
        }
        else if (sliceUploadResults.Any(success => success is FileUploadResult.Fail))
        {
            return FileUploadResult.Fail;
        }
        else
        {
            return FileUploadResult.Canceled;
        }
    }

    /// <summary>
    /// 异步切分要上传的文件
    /// <br/>如果想中途结束切分，不要在调用此方法的foreach块中使用break，请使用取消令牌，否则会出现内存泄漏
    /// </summary>
    /// <param name="file">要分片的文件</param>
    /// <param name="sliceSize">分片大小</param>
    /// <param name="cancellation">取消令牌</param>
    /// <returns>已切分的文件片段数据，用完切记释放其中的内存缓冲</returns>
    private static async IAsyncEnumerable<(IMemoryOwner<byte> memory, int sliceIndex, int readBytes, long fileOffset)> SliceFileAsync(
        IBrowserFile file,
        int sliceSize,
        [EnumeratorCancellation] CancellationToken cancellation = default)
    {
        if (cancellation.IsCancellationRequested) yield break;

        int fileSliceIndex;
        long fileOffset;
        IMemoryOwner<byte> memory;
        await using var fileStream = file.OpenReadStream(long.MaxValue);

        for (fileSliceIndex = 0, fileOffset = 0, memory = MemoryPool<byte>.Shared.Rent(sliceSize);
            (await fileStream.ReadAsync(memory.Memory[..sliceSize], cancellation)) is int readBytes and > 0;
            fileSliceIndex++, fileOffset += readBytes, memory = MemoryPool<byte>.Shared.Rent(sliceSize)
        )
        {
            if (cancellation.IsCancellationRequested)
            {
                // 如果取消切分，缓冲不会返回到外部，只能在内部释放
                memory.Dispose();
                yield break;
            }
            yield return (memory, fileSliceIndex, readBytes, fileOffset);
        }
        // 切分结束后会多出一个没用的缓冲，只能在内部释放
        memory.Dispose();
    }

    /// <summary>
    /// 上传结果
    /// </summary>
    public enum FileUploadResult
    {
        /// <summary>
        /// 失败
        /// </summary>
        Fail = -2,

        /// <summary>
        /// 取消
        /// </summary>
        Canceled = -1,

        /// <summary>
        /// 没有结果，未知结果
        /// </summary>
        None = 0,

        /// <summary>
        /// 成功
        /// </summary>
        Success = 1
    }
}